﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using DeepEarth.Core;
using DeepEarth.Core.Utilities;
using DeepEarth.Map.Core;
using DeepEarth.Toolkit.Controls.Measure;
using DeepEarth.Toolkit.Drawing;

namespace DeepEarth.Toolkit.Controls
{
    public class DistanceLabeler : INotifyPropertyChanged
    {
        IEnumerable<Location> Locations { get; set; }

        Panel DrawingSurface { get; set; }
        Point Offset { get; set; }

        List<Tuple<Location, Label, double>> labels;

        double totalDistance;
        public double TotalDistance
        {
            get
            {
                return totalDistance;
            }
            set
            {
                totalDistance = value;
                OnPropertyChanged("TotalDistance");
            }
        }

        double area;
        public double Area
        {
            get
            {
                return area;
            }
            set
            {
                area = value;
                OnPropertyChanged("Area");
            }
        }

        AreaCalculator areaCalculator;

        public DistanceLabeler(Panel drawingSurface, IEnumerable<Location> locations, Func<Location, Point> locationToViewportPoint, Func<Point, Location> viewportPointToLocation)
        {
            DrawingSurface = drawingSurface;
            Locations = locations;

            areaCalculator = new AreaCalculator(locationToViewportPoint, viewportPointToLocation);

            Offset = new Point(0, -5);

            labels = new List<Tuple<Location, Label, double>>();

            RedrawLabels();

            if (locations is INotifyCollectionChanged)
            {
                var observable = locations as INotifyCollectionChanged;
                //WeakEventListener<DistanceLabeler, INotifyCollectionChanged> wel = new WeakEventListener<DistanceLabeler, INotifyCollectionChanged>(this, observable);

                //wel.DetatchAction = (listener, source) =>
                //    {
                //        source.CollectionChanged -= listener.OnEvent;
                //    };

                //wel.EventAction = (labeller, source, eventArgs) =>
                //    {
                //        labeller.RedrawLabels();
                //    };
                //observable.CollectionChanged += wel.OnEvent;     

                var obs = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(observable, "CollectionChanged");
                obs.Throttle(TimeSpan.FromMilliseconds(250)).Subscribe(evt => DrawingSurface.Dispatcher.BeginInvoke(RedrawLabels));  

                observable.CollectionChanged += (o, e) => RedrawLabels();
            }            
        }

        void ClearLabels()
        {
            labels.Iter(t => DrawingSurface.Children.Remove(t.Item2));
            labels.Clear();
        }

        void RedrawLabels()
        {
            ClearLabels();
            UpdateLabels();           
        }

        void UpdateLabels()
        {
            if (!Locations.Any())
            {
                return;
            }

            Location firstLocation = Locations.FirstOrDefault();
            Location lastpoint = firstLocation;

            List<Location> locations = Locations.ToList();

            for (int i = 1; i < locations.Count; i++)
            {
                Location l = locations[i];
                Location midpoint = Location.GetMidpointLocation(l, lastpoint);

                Tuple<Location, Label, double> tuple;

                if (i > labels.Count)
                {
                    var lab = new Label
                    {
                        OffsetX = Offset.X,
                        OffsetY = Offset.Y,
                    };

                    var distance = Calculations.DistVincenty(l, lastpoint);              
                    lab.Text = new DistanceFormatConverter().Convert(distance);
                    tuple = new Tuple<Location, Label, double>(midpoint, lab, distance);
                    labels.Add(tuple);

                    DrawingSurface.Children.Add(lab);

                    var positionBinding = new Binding { Source = midpoint };
                    lab.SetBinding(Layer.PositionProperty, positionBinding);

                    lab.SetValue(Layer.PositionOriginProperty, new PositionOrigin(0.5, 1.0));
                    //lab.SetValue(Layer.PositionProperty, midpoint);

                    var observable = Observable.FromEventPattern<PropertyChangedEventArgs>(l, "PropertyChanged");
                    observable.Throttle(TimeSpan.FromMilliseconds(250)).Subscribe(evt => DrawingSurface.Dispatcher.BeginInvoke(UpdateLabels));                                       
                }

                tuple = labels[i-1];

                Location oldMid = tuple.Item1;
                if (tuple.Item1 != midpoint)
                {
                    oldMid.Altitude = midpoint.Altitude;
                    oldMid.Latitude = midpoint.Latitude;
                    oldMid.Longitude = midpoint.Longitude;

                    double distance = Calculations.DistVincenty(l, lastpoint);
                    labels[i - 1] = new Tuple<Location, Label, double>(oldMid, tuple.Item2, distance);
                    tuple.Item2.Text = new DistanceFormatConverter().Convert(distance);
                }

                lastpoint = l;
            }

            TotalDistance = labels.Fold((state, t) => state + t.Item3, 0.0);

            updateToken = Guid.NewGuid();

            System.Threading.ThreadPool.QueueUserWorkItem(s => UpdateArea(updateToken), updateToken);                
        }

        Guid updateToken;

        void UpdateArea(Guid token)
        {
            if (updateToken != token)
            {
                return;
            }

            if (updateToken == token)
            {
                DrawingSurface.Dispatcher.BeginInvoke(() => {
                    double area = areaCalculator.Calculate(Locations.ToList(), true);    
                    Area = area; 
                });
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
